# -*- coding: utf-8 -*-

# Standard imports.
import time, re

# Genshi imports.
from genshi.builder import tag

# Trac imports.
from trac.core import *
from trac.wiki.web_ui import WikiModule
from trac.wiki.formatter import format_to_html, format_to_oneliner
from trac.web.href import Href
from trac.web.chrome import Chrome, add_stylesheet
from trac.util import format_date, format_datetime
from trac.util.text import to_unicode
from trac.util.translation import _

# Trac interfaces.
from trac.web.main import IRequestFilter
from trac.wiki import IWikiSyntaxProvider, IWikiMacroProvider, parse_args
from trac.util.html import html

from multiproject.common.projects.listeners import IProjectChangeListener
from multiproject.common.projects.project import Project
from multiproject.project.admin.news_forum import IAnnouncementAdminListener
from multiproject.common.wiki.news import ProjectNews
from multiproject.core.db import admin_transaction

# Local imports.
from tracdiscussion.api import *
from tracdiscussion.core import *
from tracdiscussion.global_announcements import GlobalNews
from tracdiscussion.listeners import ITopicChangeListener

class DiscussionWiki(Component):
    """
        The wiki module implements macros for forums, topics and messages
        referencing.
    """
    implements(IWikiSyntaxProvider, IWikiMacroProvider, IRequestFilter)

    # Wiki macro documentations.

    view_topic_doc = _("Displays content of a discussion topic. If no argument "
      "passed tries to find the topic with the same name as is the name of the "
      "current wiki page. If the topic name is passed, displays that topic. ")

    recent_topics_doc = _("Lists all topics that have been recently active "
      "grouping them by the day they were lastly active. This macro accepts "
      "two parameters. The first is a forum ID. If provided, only topics in "
      "that forum are included in the resulting list. If omitted, topics from "
      "all forums are listed. The second parameter is a number for limiting "
      "the number of topics returned. For example, specifying a limit of 5 "
      "will result in only the five most recently active topics to be included "
      "in the list.")

    # IWikiSyntaxProvider methods

    def get_link_resolvers(self):
        yield ('forum', self._discussion_link)
        yield ('last-forum', self._discussion_link)
        yield ('topic', self._discussion_link)
        yield ('last-topic', self._discussion_link)
        yield ('message', self._discussion_link)
        yield ('topic-attachment', self._discussion_attachment_link)
        yield ('raw-topic-attachment', self._discussion_attachment_link)

    def get_wiki_syntax(self):
        return []

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'ViewTopic'
        yield 'RecentTopics'

    def get_macro_description(self, name):
        if name == 'ViewTopic':
            return self.view_topic_doc
        elif name == 'RecentTopics':
            return self.recent_topics_doc
        else:
            return ""

    def expand_macro(self, formatter, name, content):
        if name == 'ViewTopic':
            return self._expand_view_topic(formatter, name, content)
        elif name == 'RecentTopics':
            return self._expand_recent_topics(formatter, name, content)
        else:
            raise TracError('Not implemented macro %s' % (name))

    # IRequestFilter methods.

    def pre_process_request(self, req, handler):
        # Change method from POST to GET.
        match = re.match(r'^/wiki(?:/(.*))?', req.path_info)
        action = req.args.get('discussion_action')
        if match and action and req.method == 'POST':
            req.environ['REQUEST_METHOD'] = 'GET'

        # Continue processing request.
        return handler

    def post_process_request(self, req, template, data, content_type):
        return (template, data, content_type)

    # Internal methods.

    def _expand_view_topic(self, formatter, name, content):
        self.log.debug("Rendering ViewTopic macro...")

        # Check permission
        if not formatter.perm.has_permission('DISCUSSION_VIEW'):
            return

        # Determine topic subject
        page_name = formatter.req.path_info[6:] or 'WikiStart'
        subject = content or page_name

        # Create request context.
        context = Context.from_request(formatter.req)
        context.realm = 'discussion-wiki'

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        # Get API component.
        api = self.env[DiscussionApi]

        # Get topic by subject
        try:
            id = int(subject)
            topic = api.get_topic(context, id)
        except:
            topic = api.get_topic_by_subject(context, subject)
        self.log.debug('subject: %s' % (subject,))
        self.log.debug('topic: %s' % (topic,))

        # Prepare request and resource object.
        if topic:
            context.req.args['topic'] = topic['id']
            context.resource = Resource('discussion', 'topic/%s' % (topic['id']
              ,))

        # Process discussion request.
        template, data = api.process_discussion(context)

        # Return rendered template.
        data['discussion']['mode'] = 'message-list'
        data['discussion']['page_name'] = page_name
        if context.redirect_url:
            # Generate HTML elements for redirection.
            href = context.req.href(context.redirect_url[0]) + \
              context.redirect_url[1]
            self.log.debug("Redirecting to %s" % (href))
            return tag.div(tag.strong('Redirect: '),
              ' This page redirects to ', tag.a(href, href = href),
              tag.script("window.location = '" + context.req.href('discussion',
              'redirect', redirect_url = href) + "'", language = "JavaScript"),
              class_ = "system-message")
        else:
            # Render template.
            return to_unicode(Chrome(self.env).render_template(formatter.req,
              template, data, 'text/html', True))

    def _expand_recent_topics(self, formatter, name, content):
        self.log.debug("Rendering RecentTopics macro...")

        # Check permission
        if not formatter.perm.has_permission('DISCUSSION_VIEW'):
            return

        # Create request context.
        context = Context.from_request(formatter.req)
        context.realm = 'discussion-wiki'

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        # Get API object.
        api = self.env[DiscussionApi]

        # Get list of Trac users.
        context.users = api.get_users(context)

        # Parse macro arguments.
        arguments = []
        forum_id = None
        limit = 10
        if content:
            arguments = [argument.strip() for argument in content.split(',')]
        if len(arguments) == 1:
            limit = arguments[0]
        elif len(arguments) == 2:
            forum_id = arguments[0]
            limit = arguments[1]
        else:
            raise TracError("Invalid number of macro arguments.")

        # Construct and execute SQL query.
        columns = ('forum', 'topic', 'time')
        values = []
        if forum_id:
            values.append(forum_id)
        if limit:
            values.append(limit)
        values = tuple(values)
        sql = ("SELECT forum, topic, MAX(time) as max_time "
               "FROM "
               "  (SELECT forum, topic, time "
               "  FROM message "
               "  UNION "
               "  SELECT forum, id as topic, time "
               "  FROM topic)" +
               (forum_id and " WHERE forum = %s" or "") +
               "  GROUP BY topic "
               "  ORDER BY max_time DESC" +
               (limit and " LIMIT %s" or ""))
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

        # Collect recent topics.
        entries = []
        for row in context.cursor:
            row = dict(zip(columns, row))
            entries.append(row)

        self.log.debug(entries)

        # Format entries data.
        entries_per_date = []
        prevdate = None
        for entry in entries:
            date = format_date(entry['time'])
            if date != prevdate:
                prevdate = date
                entries_per_date.append((date, []))
            forum_name = api.get_forum(context, entry['forum'])['name']
            topic_subject = api.get_topic_subject(context, entry['topic'])
            entries_per_date[-1][1].append((entry['forum'], forum_name,
              entry['topic'], topic_subject))

        # Format result.
        return tag.div((tag.h3(date), tag.ul(tag.li(tag.a(forum_name, href =
          formatter.href.discussion('forum', forum_id)), ': ', tag.a(
          topic_subject, href = formatter.href.discussion('topic', topic_id)))
          for forum_id, forum_name, topic_id, topic_subject in entries)) for
          date, entries in entries_per_date)

    def _discussion_link(self, formatter, namespace, params, label):
        try:
           id = int(params)
        except:
           id = -1

        # Create request context.
        context = Context.from_request(formatter.req)
        context.realm = 'discussion-wiki'

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        if namespace == 'forum':
            columns = ('subject',)
            sql = ("SELECT f.subject "
                   "FROM forum f "
                   "WHERE f.id = %s")
            values = (id,)
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                row = dict(zip(columns, row))
                return tag.a(label, href = formatter.href.discussion('forum',
                  id), title = row['subject'].replace('"', ''))
            return tag.a(label, href = formatter.href.discussion('forum', id),
              title = label, class_ = 'missing')
        elif namespace == 'last-forum':
            columns = ('id', 'subject')
            sql = ("SELECT f.id, f.subject "
                   "FROM forum f "
                   "WHERE f.time = "
                   "  (SELECT MAX(time) "
                   "  FROM forum "
                   "  WHERE forum_group = %s)")
            values = (id,)
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                row = dict(zip(columns, row))
                return tag.a(label, href = formatter.href.discussion('forum',
                  row['id']), title = row['subject'].replace('"', ''))
            return tag.a(label, href = formatter.href.discussion('forum',
              '-1'), title = label, class_ = 'missing')
        elif namespace == 'topic':
            columns = ('forum', 'forum_subject', 'subject')
            sql = ("SELECT t.forum, f.subject, t.subject "
                   "FROM topic t "
                   "LEFT JOIN forum f "
                   "ON t.forum = f.id "
                   "WHERE t.id = %s")
            values = (id,)
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                row = dict(zip(columns, row))
                return tag.a(label, href = '%s#-1' % \
                  (formatter.href.discussion('topic', id),), title =
                  ('%s: %s' % (row['forum_subject'], row['subject']))
                  .replace('"', ''))
            return tag.a(label, href = formatter.href.discussion('topic', id),
              title = label.replace('"', ''), class_ = 'missing')
        elif namespace == 'last-topic':
            columns = ('id', 'forum_subject', 'subject')
            sql = ("SELECT t.id, f.subject, t.subject "
                   "FROM topic t "
                   "LEFT JOIN forum f "
                   "ON t.forum = f.id WHERE t.time = "
                   "  (SELECT MAX(time) "
                   "  FROM topic "
                   "  WHERE forum = %s)")
            values = (id,)
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                row = dict(zip(columns, row))
                return tag.a(label, href = '%s#-1' % \
                  (formatter.href.discussion('topic', row['id']),), title =
                  ('%s: %s' % (row['forum_subject'], row['subject']))
                  .replace('"', ''))
            return tag.a(label, href = formatter.href.discussion('topic',
              '-1'), title = label.replace('"', ''), class_ = 'missing')
        elif namespace == 'message':
            columns = ('forum', 'topic', 'forum_subject', 'subject')
            sql = ("SELECT m.forum, m.topic, f.subject, t.subject "
              "FROM message m, "
                "(SELECT subject, id "
                "FROM forum) f, "
                "(SELECT subject, id "
                "FROM topic) t "
              "WHERE m.forum = f.id AND m.topic = t.id AND m.id = %s")
            values = (id,)
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                row = dict(zip(columns, row))
                return tag.a(label, href = '%s#%s' % \
                  (formatter.href.discussion('message', id), id), title = (
                  '%s: %s' % (row['forum_subject'], row['subject'])).replace(
                  '"', ''))
            return tag.a(label, href = formatter.href.discussion('message', id),
              title = label.replace('"', ''), class_ = 'missing')
        else:
            return tag.a(label, href = formatter.href.discussion('message', id),
              title = label.replace('"', ''), class_ = 'missing')

    def _discussion_attachment_link(self, formatter, namespace, params, label):
        id, name = params.split(':')

        # Create request context.
        context = Context.from_request(formatter.req)
        context.realm = 'discussion-wiki'

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        if namespace == 'topic-attachment':
            return format_to_html(self.env, context,
              '[attachment:discussion:topic/%s:%s %s]' % (id, name, label))
        elif namespace == 'raw-topic-attachment':
            return format_to_html(self.env, context,
              '[raw-attachment:discussion:topic/%s:%s %s]' % (id, name, label))
        else:
            return tag.a(label, href = formatter.href.discussion('topic', id),
              title = label.replace('"', ''), class_ = 'missing')


class GlobalAnnouncements(Component):
    """
    Render X amount of latest events from from all projects' news forums.
    Only the latest news item from each project is shown.
    """
    implements(IWikiMacroProvider, ITemplateProvider)

    theme_location = Option('multiproject', 'theme_htdocs_location', '/htdocs/theme', 'Theme location')
    theme_name = Option('multiproject', 'theme_name', '', 'Theme name')

    # IWikiMacroProvider methods
    def get_macros(self):
        """
        Return an iterable of all macros this component provides.
        """
        yield "GlobalAnnouncements"

    def get_macro_description(self, name):
        """
        Return a plain text description of a macro, based on name.
        """
        if name != 'GlobalAnnouncements':
            return None

        return '''
Get the latest news item from all projects' "Announcements" discussion
board. Take note that only public (at the time of posting) projects'
announcements are shown.

Arguments:

* title - Title for the section to show. Defaults to "Latest Announcements".
* count - The amount of news to list. Defaults to 5.
* separator - show separator (true|false). Default is true.

Example:
{{{
[[GlobalAnnouncements(count=10)]]
}}}
Will return the last 10 news from projects
'''

    def expand_macro(self, formatter, name, content, args=None):
        """
        Execute the macro. When it is used as a Macro, and not as a wiki processor,
        the arguments to the macro are passed in content. If there is an error
        getting the content, a brief error text is printed.

        :returns: Global announcements, in html formatting.
        """
        if name != 'GlobalAnnouncements':
            return None

        data = {}

        title = None
        env = None

        # Parse settings for the macro
        data['count'], data['title'], data['separator'] = self._parse_args(args, content)

        try:
            news = GlobalNews()
            data['newsitems'] = news.get_global_news(limit=data['count'])
        except Exception, e:
            self.log.exception("Failed to read global announcements.")
            raise TracError("Couldn't read global announcements: {0}".format(e))

        data['req'] = formatter.req
        data['home_name'] = self.env.config.get('multiproject', 'sys_home_project_name')

        # Render the macro content with a genshi template. Note that errors are printed on
        # screen with some user friendly texts instead of exceptions (on most common cases
        # anyway).
        self.log.debug(data)
        return Chrome(self.env).render_template(
            formatter.req, 'discussion-global-announcements.html', data, 'text/html', True)

    # ITemplateProvide methods
    def get_templates_dirs(self):
        """
        Returns the directories where to ask for templates
        """
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        """
        Returns the directories (with a prefix) to get the htdocs
        """
        return []

    # "Private" methods
    def _parse_args(self, args, content):
        """
        Parse args from incoming args or from content. Depending on which is set.
        If called as a macro, the content holds the arguments. Otherwise it's
        the args param that holds the arguments.

        In macro case, a tuple is returned, first element is list of arguments,
        second is a dict. We only support the dict format.
        """
        count = 5
        title = 'Latest Announcements'
        separator = 'rss_separator'

        if args is None:
            args = parse_args(content)
            if len(args) > 1:
                args = args[1]

        if args is not None and 'count' in args:
            count = args['count']
            count = safe_int(count)

        if args is not None and 'title' in args:
            title = args['title']
            title = title.strip('"')

        if args is not None and 'separator' in args:
            if args['separator'] == 'true' or args['separator'] == 'True':
                separator = 'rss_separator'
            else:
                separator = 'no_rss_separator'

        self.log.debug("Returning args: {0}".format((count, title, separator)))

        return count, title, separator


class GlobalAnnouncementListener(Component):
    implements(IProjectChangeListener,
               IAnnouncementAdminListener,
               ITopicChangeListener)

    def __init__(self):
        self.env_name = self.env.path.split('/')[-1]

    def project_set_private(self, project):
        self.log.debug('Project %s(%s) was set private, cleaning global announcement from DB' % (project.project_name, project.env_name))
        self._clean_project_announcement()

    def project_created(self, project):
        pass

    def project_set_public(self, project):
        pass

    def project_archived(self, project):
        self.log.debug('Project %s(%s) was archived, cleaning global announcement from DB' % (project.project_name, project.env_name))
        self._clean_project_announcement()

    def project_deleted(self, project):
        self.log.debug('Project %s(%s) was deleted, cleaning global announcement from DB' % (project.project_name, project.env_name))
        self._clean_project_announcement()

    def announcements_hidden(self):
        self.log.debug('Project %s announcements were hidden, cleaning global announcement from DB' % (self.env.path.split('/')[-1]))
        self._clean_project_announcement()

    def announcements_visible(self):
        pass

    # TopicChangeListener
    def topic_created(self, context, topic):
        self.log.debug("Topic created in %s: %s" % (self.env_name, topic["subject"]))
        project = Project.get(self.env)
        news = ProjectNews(project.env_name)

        # Check if the forum is the announcement forum.
        if news.get_news_forum_id() != topic["forum"]:
            return

        # Check if project is public.
        # TODO: Change to check private/public flag when it comes (if is_public() isn't changed too)
        if not project.public:
            self.log.debug("Project is private, not storing the announcement")
            return

        # Check if project has hidden it's announcements.
        if not self.env.config.getbool('discussion', 'show_news_forum', True):
            self.log.debug("Project announcements hidden, not storing the announcement")
            return

        self.log.debug("Adding topic %s from %s to global announcements." % (topic["subject"], self.env_name))
        # FIXME: Tight dependency to MultiProject and trac_admin database
        context.cursor.execute("""
        REPLACE trac_admin.global_announcements
        VALUES (%s, %s, %s, %s, %s, %s, %s)
        """,
        (project.project_name, self.env_name, topic["time"], topic["subject"], topic["author"], topic["body"], topic["id"]))

    def topic_changed(self, context, topic, old_topic):
        pass

    def topic_deleted(self, context, topic):
        pass

    def _clean_project_announcement(self):
        sql = "DELETE FROM global_announcements WHERE environment_name = %s"
        with admin_transaction() as cursor:
            try:
                cursor.execute(sql, self.env_name)
            except Exception, e:
                self.log.exception("Announcement cleaning failed: {0}".format(sql))
